use ahash::HashMap;

use mago_codex::ttype::add_union_type;
use mago_codex::ttype::atomic::TAtomic;
use mago_codex::ttype::atomic::callable::TCallable;
use mago_codex::ttype::comparator::ComparisonResult;
use mago_codex::ttype::comparator::union_comparator::can_expression_types_be_identical;
use mago_codex::ttype::comparator::union_comparator::is_contained_by;
use mago_codex::ttype::union::TUnion;
use mago_codex::ttype::*;
use mago_reporting::Annotation;
use mago_reporting::Issue;
use mago_span::HasSpan;
use mago_span::Span;
use mago_syntax::ast::*;

use crate::analyzable::Analyzable;
use crate::artifacts::AnalysisArtifacts;
use crate::code::IssueCode;
use crate::context::Context;
use crate::context::block::BlockContext;
use crate::error::AnalysisError;
use crate::invocation::InvocationTarget;
use crate::utils::get_type_diff;

/// Checks if an argument can be passed by reference.
fn is_argument_referenceable(argument_expression: &Expression, argument_type: &TUnion) -> bool {
    argument_expression.is_referenceable(false)
        || (argument_expression.is_referenceable(true) && argument_type.by_reference())
}

fn is_empty_container_construction(expression: &Expression) -> bool {
    if let Expression::Instantiation(instantiation) = expression
        && let Some(argument_list) = &instantiation.argument_list
        && argument_list.arguments.len() == 1
        && let Some(first_arg) = argument_list.arguments.iter().next()
    {
        let argument_value = first_arg.value();

        match argument_value {
            Expression::Array(array_expr) => array_expr.elements.is_empty(),
            Expression::LegacyArray(array_expr) => array_expr.elements.is_empty(),
            _ => false,
        }
    } else {
        false
    }
}

/// Analyzes an argument expression and stores its inferred type.
pub fn analyze_and_store_argument_type<'ctx, 'arena>(
    context: &mut Context<'ctx, 'arena>,
    block_context: &mut BlockContext<'ctx>,
    artifacts: &mut AnalysisArtifacts,
    invocation_target: &InvocationTarget<'ctx>,
    argument_expression: &Expression<'arena>,
    argument_offset: usize,
    analyzed_argument_types: &mut HashMap<usize, (TUnion, Span)>,
    referenced_parameter: bool,
    closure_parameter_type: Option<&TUnion>,
) -> Result<(), AnalysisError> {
    if argument_offset != usize::MAX && analyzed_argument_types.contains_key(&argument_offset) {
        return Ok(());
    }

    let inferred_parameter_types = closure_parameter_type.map(|closure_parameter_type| {
        let mut inferred_parameters = HashMap::default();

        closure_parameter_type
            .types
            .as_ref()
            .iter()
            .filter_map(|atomic| match atomic {
                TAtomic::Callable(TCallable::Signature(callable)) => Some(callable),
                _ => None,
            })
            .flat_map(|callable| callable.parameters.iter().enumerate())
            .filter_map(|(parameter_index, parameter)| {
                parameter
                    .get_type_signature()
                    .filter(|param_type| param_type.is_array() || param_type.has_object())
                    .map(|param_type| (parameter_index, param_type.clone()))
            })
            .for_each(|(parameter_index, parameter_type)| {
                inferred_parameters.insert(parameter_index, parameter_type);
            });

        inferred_parameters
    });

    let inferred_parameter_types = std::mem::replace(&mut artifacts.inferred_parameter_types, inferred_parameter_types);

    let was_inside_general_use = block_context.inside_general_use;
    let was_inside_call = block_context.inside_call;
    let was_inside_variable_reference = block_context.inside_variable_reference;

    block_context.inside_general_use = true;
    block_context.inside_call = true;
    block_context.inside_variable_reference = referenced_parameter;

    argument_expression.analyze(context, block_context, artifacts)?;

    block_context.inside_general_use = was_inside_general_use;
    block_context.inside_call = was_inside_call;
    block_context.inside_variable_reference = was_inside_variable_reference;
    artifacts.inferred_parameter_types = inferred_parameter_types;

    let argument_type = artifacts.get_expression_type(argument_expression).cloned().unwrap_or_else(get_mixed);

    if referenced_parameter && !is_argument_referenceable(argument_expression, &argument_type) {
        let target_kind_str = invocation_target.guess_kind();
        let target_name_str = invocation_target.guess_name();

        context.collector.report_with_code(
            IssueCode::InvalidPassByReference,
            Issue::error(format!(
                "Invalid argument for by-reference parameter #{} in call to {} `{}`.",
                argument_offset + 1,
                target_kind_str,
                target_name_str,
            ))
            .with_annotation(
                Annotation::primary(argument_expression.span())
                    .with_message("This expression cannot be passed by reference."),
            )
            .with_note(
                "You can only pass variables, properties, array elements, or the result of another function that itself returns a reference."
            )
            .with_help("To fix this, assign this value to a variable first, and then pass that variable to the function."),
        );
    }

    if argument_offset != usize::MAX {
        analyzed_argument_types.insert(argument_offset, (argument_type, argument_expression.span()));
    }

    Ok(())
}

/// Verifies an argument's type against the expected parameter type.
pub fn verify_argument_type<'ctx, 'ast, 'arena>(
    context: &mut Context<'ctx, 'arena>,
    input_type: &TUnion,
    parameter_type: &TUnion,
    argument_offset: usize,
    input_expression: &'ast Expression<'arena>,
    invocation_target: &InvocationTarget<'_>,
) {
    let target_kind_str = invocation_target.guess_kind();
    let target_name_str = invocation_target.guess_name();

    if input_type.is_never() {
        context.collector.report_with_code(
            IssueCode::NoValue,
            Issue::error(format!(
                "Argument #{} passed to {} `{}` has type `never`, meaning it cannot produce a value.",
                argument_offset + 1,
                target_kind_str,
                target_name_str
            ))
            .with_annotation(
                Annotation::primary(input_expression.span())
                    .with_message("This argument expression results in type `never`")
            )
            .with_note(
                "The `never` type indicates this expression will not complete to produce a value."
            )
            .with_note(
                "This often occurs in unreachable code, due to impossible conditional logic, or if an expression always exits (e.g., `throw`, `exit()`)."
            )
            .with_help(
                "Review preceding logic to ensure this argument can receive a value, or remove if unreachable."
            ),
        );

        return;
    }

    let call_site = Annotation::secondary(invocation_target.span())
        .with_message(format!("Arguments to this {} are incorrect", invocation_target.guess_kind()));

    let input_type_str = input_type.get_id();
    let parameter_type_str = parameter_type.get_id();

    if !parameter_type.accepts_null() {
        if input_type.is_null() {
            context.collector.report_with_code(
                IssueCode::NullArgument,
                Issue::error(format!(
                    "Argument #{} of {} `{}` is `null`, but parameter type `{}` does not accept it.",
                    argument_offset + 1,
                    target_kind_str,
                    target_name_str,
                    parameter_type_str
                ))
                .with_annotation(Annotation::primary(input_expression.span()).with_message("This argument is `null`"))
                .with_annotation(call_site)
                .with_help(format!(
                    "Provide a non-null value, or declare the parameter as nullable (e.g., `{parameter_type_str}|null`)."
                )),
            );

            return;
        }

        if input_type.is_nullable() && !input_type.ignore_nullable_issues() {
            context.collector.report_with_code(
                IssueCode::PossiblyNullArgument,
                Issue::error(format!(
                    "Argument #{} of {} `{}` is possibly `null`, but parameter type `{}` does not accept it.",
                    argument_offset + 1,
                    target_kind_str,
                    target_name_str,
                    parameter_type_str
                ))
                .with_annotation(
                    Annotation::primary(input_expression.span())
                        .with_message(format!("This argument of type `{input_type_str}` might be `null`")),
                )
                .with_annotation(call_site.clone())
                .with_help("Add a `null` check before this call to ensure the value is not `null`."),
            );
        }
    }

    if !parameter_type.accepts_false() {
        if input_type.is_false() {
            context.collector.report_with_code(
                IssueCode::FalseArgument,
                Issue::error(format!(
                    "Argument #{} of {} `{}` is `false`, but parameter type `{}` does not accept it.",
                    argument_offset + 1,
                    target_kind_str,
                    target_name_str,
                    parameter_type_str
                ))
                .with_annotation(Annotation::primary(input_expression.span()).with_message("This argument is `false`"))
                .with_annotation(call_site)
                .with_help(format!(
                    "Provide a different value, or update the parameter type to accept false (e.g., `{parameter_type_str}|false`)."
                )),
            );

            return;
        }

        if input_type.is_falsable() && !input_type.ignore_falsable_issues() {
            context.collector.report_with_code(
                IssueCode::PossiblyFalseArgument,
                Issue::error(format!(
                    "Argument #{} of {} `{}` is possibly `false`, but parameter type `{}` does not accept it.",
                    argument_offset + 1,
                    target_kind_str,
                    target_name_str,
                    parameter_type_str
                ))
                .with_annotation(
                    Annotation::primary(input_expression.span())
                        .with_message(format!("This argument of type `{input_type_str}` might be `false`")),
                )
                .with_annotation(call_site.clone())
                .with_help("Add a check to ensure the value is not `false` before this call."),
            );
        }
    }

    let mut union_comparison_result = ComparisonResult::new();
    let type_match_found =
        is_contained_by(context.codebase, input_type, parameter_type, true, true, false, &mut union_comparison_result);

    if type_match_found {
        return;
    }

    if input_type.is_mixed() {
        context.collector.report_with_code(
            IssueCode::MixedArgument,
            Issue::error(format!(
                "Invalid argument type for argument #{} of `{}`: expected `{}`, but found `{}`.",
                argument_offset + 1,
                target_name_str,
                parameter_type_str,
                input_type_str
            ))
            .with_annotation(
                Annotation::primary(input_expression.span())
                    .with_message(format!("Argument has type `{input_type_str}`")),
            )
            .with_annotation(call_site)
            .with_note(format!(
                "The type `{input_type_str}` is too general and does not match the expected type `{parameter_type_str}`."
            ))
            .with_help("Add specific type hints or assertions to the argument value."),
        );

        return;
    }

    let is_empty_container = input_type.is_empty_array() || is_empty_container_construction(input_expression);
    if union_comparison_result.type_coerced.unwrap_or(false) && !input_type.is_mixed() && !is_empty_container {
        let issue_kind;
        let annotation_msg;
        let note_msg;

        if union_comparison_result.type_coerced_from_nested_mixed.unwrap_or(false) {
            issue_kind = IssueCode::LessSpecificNestedArgumentType;
            annotation_msg = format!("Provided type `{input_type_str}` is too general due to nested `mixed`.");
            note_msg = "The structure contains `mixed`, making it incompatible.".to_string();
        } else {
            issue_kind = IssueCode::LessSpecificArgument;
            annotation_msg = format!("Provided type `{input_type_str}` is too general.");
            note_msg = format!(
                    "The provided type `{input_type_str}` can be assigned to `{parameter_type_str}`, but is wider (less specific)."
                )
                .to_string();
        }

        let mut issue = Issue::error(format!(
            "Argument type mismatch for argument #{} of `{}`: expected `{}`, but provided type `{}` is less specific.",
            argument_offset + 1,
            target_name_str,
            parameter_type_str,
            input_type_str
        ))
        .with_annotation(Annotation::primary(input_expression.span()).with_message(annotation_msg))
        .with_annotation(call_site)
        .with_note(note_msg)
        .with_help(format!(
            "Provide a value that more precisely matches `{parameter_type_str}` or adjust the parameter type."
        ));

        if let Some(type_diff) = get_type_diff(context, parameter_type, input_type) {
            issue = issue.with_note(type_diff);
        }

        context.collector.report_with_code(issue_kind, issue);
    } else if !union_comparison_result.type_coerced.unwrap_or(false) {
        let types_can_be_identical =
            can_expression_types_be_identical(context.codebase, input_type, parameter_type, false, false);

        let kind;
        let mut issue;
        if types_can_be_identical {
            kind = IssueCode::PossiblyInvalidArgument;

            issue = Issue::error(format!(
                "Possible argument type mismatch for argument #{} of `{}`: expected `{}`, but possibly received `{}`.",
                argument_offset + 1,
                target_name_str,
                parameter_type_str,
                input_type_str
            ))
            .with_annotation(
                Annotation::primary(input_expression.span())
                    .with_message(format!("This might not be type `{parameter_type_str}`")),
            )
            .with_annotation(call_site)
            .with_note(format!(
                "The provided type `{input_type_str}` overlaps with `{parameter_type_str}` but is not fully contained."
            ))
            .with_help("Ensure the argument always has the expected type using checks or assertions.");
        } else {
            kind = IssueCode::InvalidArgument;
            issue = Issue::error(format!(
                "Invalid argument type for argument #{} of `{}`: expected `{}`, but found `{}`.",
                argument_offset + 1,
                target_name_str,
                parameter_type_str,
                input_type_str
            ))
            .with_annotation(
                Annotation::primary(input_expression.span()).with_message(format!("This has type `{input_type_str}`")),
            )
            .with_annotation(call_site)
            .with_note(format!(
                "The provided type `{input_type_str}` is not compatible with the expected type `{parameter_type_str}`."
            ))
            .with_help(format!(
                "Change the argument value to match `{parameter_type_str}`, or update the parameter's type declaration."
            ));
        }

        if let Some(type_diff) = get_type_diff(context, parameter_type, input_type) {
            issue = issue.with_note(type_diff);
        }

        context.collector.report_with_code(kind, issue);
    }
}

/// Gets the element type when unpacking an argument with the spread operator.
pub fn get_unpacked_argument_type<'ctx, 'arena>(
    context: &mut Context<'ctx, 'arena>,
    argument_value_type: &TUnion,
    span: Span,
) -> TUnion {
    let mut potential_element_types = Vec::new();
    let mut reported_an_error = false;

    for atomic_type in argument_value_type.types.as_ref() {
        if let Some(value_parameter) = get_iterable_value_parameter(atomic_type, context.codebase) {
            potential_element_types.push(value_parameter);

            continue;
        }

        match atomic_type {
            TAtomic::Never => {
                potential_element_types.push(get_never());
            }
            TAtomic::Mixed(_) => {
                if !reported_an_error {
                    context.collector.report_with_code(
                        IssueCode::MixedArgument,
                        Issue::error(format!(
                            "Cannot unpack argument of type `{}` because it is not guaranteed to be iterable.",
                            atomic_type.get_id()
                        ))
                        .with_annotation(Annotation::primary(span).with_message("Expected an `iterable` for unpacking"))
                        .with_note("Argument unpacking `...` requires an `iterable` (e.g., `array` or `Traversable`).")
                        .with_note("The type `mixed` provides no guarantee of iterability.")
                        .with_help("Ensure the value is an `iterable` using type hints, checks, or assertions."),
                    );
                    reported_an_error = true;
                }

                potential_element_types.push(get_mixed());
            }
            _ => {
                if !reported_an_error {
                    let type_str = atomic_type.get_id();
                    context.collector.report_with_code(
                        IssueCode::InvalidArgument,
                        Issue::error(format!(
                            "Cannot unpack argument of type `{type_str}` because it is not an iterable type."
                        ))
                        .with_annotation(
                            Annotation::primary(span).with_message(format!("Type `{type_str}` is not `iterable`")),
                        )
                        .with_note("Argument unpacking `...` requires an `iterable` (e.g., `array` or `Traversable`).")
                        .with_help("Ensure the value being unpacked is an `iterable`."),
                    );
                    reported_an_error = true;
                }
                potential_element_types.push(get_mixed());
            }
        }
    }

    potential_element_types
        .into_iter()
        .reduce(|acc, element_type| add_union_type(acc, &element_type, context.codebase, false))
        .unwrap_or_else(get_never)
}
